package com.linkstec.util;

import lombok.extern.slf4j.Slf4j;
//import org.apache.commons.collections4.CollectionUtils;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * redis工具类
 *
 * @author
 */
@Slf4j
@SuppressWarnings("unchecked")
public class RedisUtil {

    private static RedisUtil redisUtil;

    public static RedisUtil getInstance() {
        return redisUtil;
    }

    public RedisUtil(String keyNameSpace, RedisTemplate<String, Object> redisTemplate) {
        Assert.notNull(keyNameSpace, "redis keyNameSpace must not be null");
        Assert.notNull(redisTemplate, "redisTemplate must not be null");
        this.keyNameSpace = keyNameSpace;
        RedisUtil.redisTemplate = redisTemplate;
        redisUtil = this;
    }

    private static RedisTemplate<String, Object> redisTemplate;
    private String keyNameSpace;
    private long warnTime = 3000L;

    /**
     * @return
     */
    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }

    /**
     * 设置键值对
     *
     * @param key
     * @param value
     */
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(prefixKey(key), value);
    }

    public void setExpire(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(prefixKey(key), value, timeout, unit);
    }

    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return redisTemplate.expire(prefixKey(key), timeout, unit);
    }

    /**
     * 设置过期时间
     *
     * @param key
     * @param date
     */
    public void expireAt(String key, Date date) {
        redisTemplate.expireAt(prefixKey(key), date);
    }

    /**
     * 次日凌晨0点过期
     *
     * @param key
     */
    public void expireAtMorrowZero(String key) {
        expireAt(key, morrowZero());
    }

    /**
     * 根据key获取值
     *
     * @param key
     * @return
     */
	public <T> T get(String key) {
        return (T) redisTemplate.opsForValue().get(prefixKey(key));
    }

    /**
     * @param key
     * @param hashKey
     * @param <T>
     * @return
     */
    public <T> T hashGet(String key, String hashKey) {
        return (T) redisTemplate.opsForHash().get(prefixKey(key), hashKey);
    }

    /**
     * @param key
     * @param secondKeys
     * @param <T>
     * @return
     */
    public <T> Map<String, T> hashMultiGet(String key, Collection<String> secondKeys) {
        Map<String, T> results = new HashMap<>(secondKeys.size());
        if (secondKeys.size() > 0) {
            Collection<Object> multiGetParam2 =
                    secondKeys.stream().map(e -> (Object) e).collect(Collectors.toList());
            List<Object> redisData = redisTemplate.opsForHash().multiGet(prefixKey(key), multiGetParam2);
            int i = 0;
            for (String secondKey : secondKeys) {
                results.put(secondKey, (T) redisData.get(i++));
            }
        }
        return results;
    }

    /**
     * 设置缓存
     *
     * <p>同时将多个 field-value (域-值)对设置到哈希表 key 中。
     *
     * <p>此命令会覆盖哈希表中已存在的域。
     *
     * <p>如果 key 不存在，一个空哈希表被创建并执行 HMSET 操作。
     *
     * @param key
     * @param map
     */
    public void hashMultiSet(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(prefixKey(key), map);
    }

    /**
     * 从列表的左边push一个元素
     *
     * @param key
     * @param value
     * @return
     */
    public long listPush(String key, Object value) {
        return redisTemplate.opsForList().leftPush(prefixKey(key), value);
    }

    /**
     *
     * @param key
     * @param data
     * @param <T>
     * @return
     */
    public <T> Long listPushAll(String key, List<T> data) {
        long startTime = System.currentTimeMillis();
        try {
            if (null != data && !data.isEmpty()) {
                return redisTemplate
                        .opsForList()
                        .leftPushAll(this.prefixKey(key), data.toArray(new Object[0]));
            }
            return 0L;
        } finally {
            long spendTime = System.currentTimeMillis() - startTime;
            if (spendTime > warnTime) {
                log.warn("slowly key:{}  spend time:{}ms", key, spendTime);
            }
        }
    }

    /**
     * 从列表的右边pop一个元素,使用pipelined的方式批量拉取数据
     *
     * @param key  队列名字
     * @param size 取数的大小
     * @return
     */
    @SuppressWarnings("rawtypes")
	public <T> List<T> listPopPipelined(String key, Integer size) {
        RedisSerializer keySerializer = redisTemplate.getKeySerializer();
        RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
        List<Object> data =
                redisTemplate.executePipelined(
                        (RedisCallback<?>)
                                redisConnection -> {
                                    for (int i = 0; i < size; i++) {
                                        redisConnection.listCommands().rPop(keySerializer.serialize(this.prefixKey(key)));
                                    }
                                    return null;
                                },
                        valueSerializer);
        List<T> res = data.stream().filter(Objects::nonNull).map(i -> (T) i).collect(Collectors.toList());
        return res.size() == 0 ? null : res;
    }

    public Object listPop(String key) {
        return redisTemplate.opsForList().rightPop(prefixKey(key));
    }

    /**
     * 获取List队列大小
     *
     * @param key
     * @return
     */
    public Long listSize(String key) {
        return redisTemplate.opsForList().size(prefixKey(key));
    }

    /**
     * LRANGE key start stop
     *
     * <p>返回列表 key 中指定区间内的元素，区间以偏移量 start 和 stop 指定。
     *
     * <p>下标(index)参数 start 和 stop 都以 0 为底，也就是说，以 0 表示列表的第一个元素，以 1 表示列表的第二个元素，以此类推。
     *
     * <p>你也可以使用负数下标，以 -1 表示列表的最后一个元素， -2 表示列表的倒数第二个元素，以此类推。
     *
     * <p>注意LRANGE命令和编程语言区间函数的区别
     *
     * <p>假如你有一个包含一百个元素的列表，对该列表执行 LRANGE list 0 10 ，结果是一个包含11个元素的列表，这表明 stop 下标也在 LRANGE
     * 命令的取值范围之内(闭区间)，这和某些语言的区间函数可能不一致，比如Ruby的 Range.new 、 Array#slice 和Python的 range() 函数。
     *
     * <p>超出范围的下标
     *
     * <p>超出范围的下标值不会引起错误。
     *
     * <p>如果 start 下标比列表的最大下标 end ( LLEN list 减去 1 )还要大，那么 LRANGE 返回一个空列表。
     *
     * <p>如果 stop 下标比 end 下标还要大，Redis将 stop 的值设置为 end 。
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public List<Object> listRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(prefixKey(key), start, end);
    }

    /**
     * LTRIM key start stop
     *
     * <p>对一个列表进行修剪(trim)，就是说，让列表只保留指定区间内的元素，不在指定区间之内的元素都将被删除。
     *
     * <p>举个例子，执行命令 LTRIM list 0 2 ，表示只保留列表 list 的前三个元素，其余元素全部删除。
     *
     * <p>下标(index)参数 start 和 stop 都以 0 为底，也就是说，以 0 表示列表的第一个元素，以 1 表示列表的第二个元素，以此类推。
     *
     * <p>你也可以使用负数下标，以 -1 表示列表的最后一个元素， -2 表示列表的倒数第二个元素，以此类推。
     *
     * <p>当 key 不是列表类型时，返回一个错误。
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public void listTrim(String key, long start, long end) {
        redisTemplate.opsForList().range(prefixKey(key), start, end);
    }

    /**
     * BRPOP 是列表的阻塞式(blocking)弹出原语。
     *
     * <p>它是 RPOP 命令的阻塞版本，当给定列表内没有任何元素可供弹出的时候，连接将被 BRPOP 命令阻塞，直到等待超时或发现可弹出元素为止。
     *
     * <p>当给定多个 key 参数时，按参数 key 的先后顺序依次检查各个列表，弹出第一个非空列表的尾部元素。
     *
     * <p>关于阻塞操作的更多信息，请查看 BLPOP 命令， BRPOP 除了弹出元素的位置和 BLPOP 不同之外，其他表现一致。
     *
     * @param key
     * @return T
     */
    public <T> T listBlockingPop(String key) {
        return (T)
                redisTemplate.opsForList().rightPop(prefixKey(key), Long.MAX_VALUE, TimeUnit.SECONDS);
    }

    /**
     * 只在键不存在时，才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
     *
     * @param key
     * @param value
     */
    public Boolean setnx(String key, Object value) {
        return redisTemplate.opsForValue().setIfAbsent(prefixKey(key), value);
    }

    /**
     * 将一个或多个 member 元素加入到集合 key 当中，已经存在于集合的 member 元素将被忽略。
     *
     * <p>假如 key 不存在，则创建一个只包含 member 元素作成员的集合。
     *
     * <p>当 key 不是集合类型时，返回一个错误。
     *
     * @param key
     * @param values
     * @return 被添加到集合中的新元素的数量，不包括被忽略的元素。
     */
    public Long setAdd(String key, Object... values) {
        return redisTemplate.opsForSet().add(prefixKey(key), values);
    }

    /**
     * 返回集合 key 的基数(集合中元素的数量)。
     *
     * @return
     */
    public Long setSize(String key) {
        return redisTemplate.opsForSet().size(prefixKey(key));
    }

    /**
     * 获取set
     *
     * @param key
     * @return
     */
    public Set<Object> setMembers(String key) {
        return redisTemplate.opsForSet().members(prefixKey(key));
    }

    /**
     * 递增1
     *
     * @param key
     * @return
     */
    public Long incrByOne(String key) {
        return this.incr(key, 1L);
    }

    /**
     * 将 key 所储存的值加上增量 increment 。
     *
     * <p>如果 key 不存在，那么 key 的值会先被初始化为 0 ，然后再执行 INCRBY 命令。
     *
     * <p>如果值包含错误的类型，或字符串类型的值不能表示为数字，那么返回一个错误。
     *
     * <p>本操作的值限制在 64 位(bit)有符号数字表示之内。
     *
     * @param key
     * @param stepVal
     * @return 加上 increment 之后， key 的值。
     */
    public Long incr(String key, Long stepVal) {
        return redisTemplate.opsForValue().increment(prefixKey(key), stepVal);
    }

    /**
     * 删除某个键值
     *
     * @param key
     */
    public void delete(String key) {
        redisTemplate.delete(keyNameSpace + key);
    }

    /**
     * @param key      加锁key
     * @param value    加锁value
     * @param time     过期时间值
     * @param timeUnit 过期时间单位
     * @return
     */
    public Boolean tryLock(String key, String value, long time, TimeUnit timeUnit) {
        // 尝试使用SETNX加锁，如果成功给锁增加一个过期时间
        return redisTemplate.opsForValue().setIfAbsent(prefixKey(key), value, time, timeUnit);
    }

    /**
     * 次日凌晨时间
     */
    public Date morrowZero() {
        Calendar c = Calendar.getInstance();
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.MILLISECOND, 0);
        c.add(Calendar.DAY_OF_MONTH, 1);
        return c.getTime();
    }

    /**
     * 处理key前缀
     *
     * @param key
     * @return
     */
    public String prefixKey(String key) {
        return keyNameSpace + key;
    }
    /**
	 * 
	 * @param key
	 * @return
	 */
	public static Boolean exists(String key) {
		return redisTemplate.hasKey(RedisUtil.getInstance().prefixKey(key));
	}

	/**
	 *
	 *
	 * <p>
	 * 此命令会覆盖哈希表中已存在的域。
	 *
	 * <p>
	 * 如果 key 不存在，一个空哈希表被创建并执行put 操作。
	 *
	 * @param key
	 * @param map
	 */
	public static void hashPut(String key, String hashKey, String value) {
		redisTemplate.opsForHash().put(RedisUtil.getInstance().prefixKey(key), hashKey, value);
	}

	/**
	 * 使用pipelined的方式设置Set
	 *
	 * @param list
	 * @return
	 */
	public static List<Object> pipelinedsAdd(List<Map<String, String>> list) {
		List<Object> data = redisTemplate.executePipelined((RedisCallback<?>) redisConnection -> {
			for (Map<String, String> item : list) {
				redisConnection.setCommands().sAdd(RedisUtil.getInstance().prefixKey(item.get("key")).getBytes(),
						item.get("value").getBytes());
				if (null != item.get("expireTime")) {
					redisConnection.keyCommands().expire(RedisUtil.getInstance().prefixKey(item.get("key")).getBytes(),
							Long.parseLong(item.get("expireTime")));
				}
			}
			return null;
		});
		return data;
	}

	/**
	 * 使用pipelined的方式设置zSet
	 *
	 * @param list
	 * @return
	 */
	public static List<Object> pipelinedzAdd(List<Map<String, String>> list) {
		List<Object> data = redisTemplate.executePipelined((RedisCallback<?>) redisConnection -> {
			for (Map<String, String> item : list) {
				redisConnection.zAdd(RedisUtil.getInstance().prefixKey(item.get("key")).getBytes(),
						Double.parseDouble(item.get("score")), item.get("value").getBytes());
				if (null != item.get("expireTime")) {
					redisConnection.keyCommands().expire(RedisUtil.getInstance().prefixKey(item.get("key")).getBytes(),
							Long.parseLong(item.get("expireTime")));
				}
			}
			return null;
		});
		return data;
	}

	/**
	 * 使用pipelined的方式设置Bit
	 *
	 * @param list
	 * @return
	 */
	public static List<Object> pipelinedsetBit(List<Map<String, String>> list) {
		List<Object> data = redisTemplate.executePipelined((RedisCallback<?>) redisConnection -> {
			for (Map<String, String> item : list) {
				redisConnection.stringCommands().setBit(RedisUtil.getInstance().prefixKey(item.get("key")).getBytes(),
						Long.parseLong(item.get("offset")), true);
				if (null != item.get("expireTime")) {
					redisConnection.keyCommands().expire(RedisUtil.getInstance().prefixKey(item.get("key")).getBytes(),
							Long.parseLong(item.get("expireTime")));
				}
			}
			return null;
		});
		return data;
	}

	/**
	 * 
	 * @param key
	 * @param value
	 * @param score
	 */
	public static void ZSetAdd(String key, String value, double score) {
		redisTemplate.opsForZSet().add(RedisUtil.getInstance().prefixKey(key), value, score);
	}

	/**
	 * 
	 * @param key
	 * @param offset
	 * @param value
	 */
	public static void setBit(String key, long offset, boolean value) {
		redisTemplate.opsForValue().setBit(RedisUtil.getInstance().prefixKey(key), offset, value);
	}
}